How we do iOS apps: Part 2 - Test Driven Development
This is the second post in a series where we describe how we build iOS apps at AppFoundry.
In this post we’ll explain Test Driven Development.
And the production code:
Now remember that while we were developing this specific class and method we used small cycles, ultimately ending to this result.
TDD
In a nutshell TDD (Test Driven Development) can be explained in a single line:write a test before you code
. However a common mistake is that TDD is not about writing a full test suite, but about writing your code in small cycles. These small cycles all start and end with unit tests, so let’s dive into that.
A unit test
A unit test is a simple test that verifies an individual unit of code, usually methods, to behave exactly as you expect. Let’s first look at some unit-tested code before we dive into the details of writing those tests. In this piece of code we are checking if the method incrementNumber on the NumberIncrementer object is returning the given integer parameter plus 1. First the unit test:import XCTest
@testable import TDD
class NumberIncrementerTest: XCTestCase {
private let numberIncrementer:NumberIncrementer = NumberIncrementer()
func testIncrementNumberShouldReturnGivenNumberPlusOne() {
let givenNumber:Int = 10
let expectedResult:Int = 11
XCTAssert(numberIncrementer.incrementNumber(givenNumber) == expectedResult)
}
}
class NumberIncrementer {
func incrementNumber(numberToIncrement: Int) -> Int {
return numberToIncrement + 1
}
}
The TDD Cycle
Step one, make a test (red phase)
Making a test is the first step in the TDD cycle. We are going to produce a failing test without touching or writing any production code. Why? Because you have to know this test fails in some circumstances and that after this step your production code solves that problem. Note that writing a test with a compilation error is also a failing test. Now it’s time to continue to the next step.Step two, write production code (green phase)
Now that we are sure the test is failing, we can make it pass by writing some production code. However, your only goal in this phase is to make your test work. Do not touch any other code but try to make it green (non failing) the easiest way possible. By following these rules you are not writing any other non-tested code. Surely you could argue that you know some piece of code is necessary to make the function complete. But you won’t need it at this time, what you do need is a green test! Once finished, continue to the last step.Step three, refactor if needed
Now it’s time to look at the code you created. Are you happy with it? If the answer is ‘no’ it’s time to do some refactoring and cheer up your mind. Don’t forget to run your tests after, to ensure your refactor didn’t break any tests. Hooray, cycle completed. Job well done! Return to step one and finish up that code you are working on.Useful iOS libraries
Libraries can really help and speed up your development process. Luckily for us, iOS developers, the community provides some great tools:SwiftHamcrest/OCHamcrest
The Hamcrest libraries provide matchers to give fluent api and improved error messages from your tests. It originated from the Java world but is also very common in other programming languages. Here’s an example of how easily these matchers are used and read:func testAvailableLanguagesContainsExpectedLanguages() {
let hamcrestSupportedLanguages = ["Java", "Python", "Ruby", "Objective-C", "PHP", "Erlang", "Swift"]
assertThat(array, containsInAnyOrder("Objective-C", "Swift", "Java", "Python", "Ruby", "PHP", "Erlang"))
}
OCMockito
Another great example of a test library is OCMockito. Unfortunately it is only available for Objective-C due to the lack of Swift runtime access, but never lose hope! For those not yet familiar with the concept of mocking I’ll provide a few words. Mocking is mainly used when your object-under-test has dependencies on other objects. Meaning these mocked dependencies will simulate the behavior of real objects. You are simply making sure your object-under-test does not rely on a dependencies’ implementation. An other example, but this time in good old Objective-C:@protocol Greeter <NSObject>
- (NSString *)sayHelloTo:(NSString *)helloText;
@end
@interface Person() {
id<Greeter> _greeter;
}
@end
@implementation Person
- (instancetype)initWithGreeter:(id<Greeter>)greeter {
self = [super init];
if (self) {
_greeter = greeter;
}
return self;
}
- (NSString *)sayHello {
return [_greeter sayHelloTo:@"OCMockito"];
}
@end
@interface PersonTest : XCTestCase {
Person *_objectUnderTest;
id<Greeter> _mockedGreeter;
}
@end
@implementation PersonTest
- (void)setUp {
_mockedGreeter = mockProtocol(@protocol(Greeter));
_objectUnderTest = [[Person alloc] initWithGreeter:_mockedGreeter];
}
- (void)testPersonGreeterSaysExpectedString {
NSString *expectedGreeting = @"expectedGreeting";
[given([_mockedGreeter sayHelloTo:@"OCMockito"]) willReturn:expectedGreeting];
XCTAssertEqualObjects([_objectUnderTest sayHello], expectedGreeting);
}
@end